#include "state_change_handler.h"
#include "logger.h"

namespace netaos {
namespace nosal {
namespace exec {
namespace internal {

namespace {

netaos::nosal::log::Logger& GetLogger()
{
    static netaos::nosal::log::Logger& logger
        = netaos::nosal::log::CreateLogger("EMSH", "StateChangeHandler", netaos::nosal::exec::internal::kLogLevel);
    return logger;
}

// map ProcessState enum to printable String.
std::map<ProcessState, std::string> processStateString{
    {ProcessState::kIdle, "Idle"},
    {ProcessState::kStarting, "Starting"},
    {ProcessState::kRunning, "Running"},
    {ProcessState::kTerminating, "Terminating"},
    {ProcessState::kTerminated, "Terminated"}
};

}  // namespace

StateChangeHandler::StateChangeHandler(std::map<std::string, FunctionGroup>& function_groups,
    std::set<std::shared_ptr<StartupConfiguration>>& executed_startup_configurations)
    : executed_startup_configurations_(executed_startup_configurations)
    , function_groups_(function_groups)
{ }

//netaos::nosal::core::Optional<StateClientImpl::SetStateResponse> 
bool StateChangeHandler::SetState(
    ProcessedSetStateRequest& processedRequest)
{
#if 0
    StateClientImpl::SetStateResponse result;

    if (!processedRequest.ongoingSetState) {
        const StateClientImpl::StateChange& stateChange = processedRequest.request.change;

        processedRequest.state = stateChange.newStateName;
        processedRequest.stateFailureOccured = false;
        processedRequest.unexpectedTerminationOnEnterOccured = false;
        processedRequest.timeoutOccured = false;
        processedRequest.failureInformation = {};

        GetLogger().LogInfo() << "Activating" << stateChange.functionGroupName + "." + stateChange.newStateName;

        // Specified function group state are changed immediately but real transition is done later.
        // Note: flags FunctionGroup::IsTransitionFinished() and FunctionGroup::IsCurrentStateFailed()
        // represent actual state of a function group.
        bool foundConfigurationError = false;

        const auto& groupIt = function_groups_.find(stateChange.functionGroupName);
        if (function_groups_.cend() == groupIt) {
            GetLogger().LogError() << "SetState called with an invalid Function Group name"
                                   << stateChange.functionGroupName;
            foundConfigurationError = true;
        } else if (!groupIt->second.HasState(processedRequest.state)) {
            GetLogger().LogError() << "SetState called with an invalid Function Group State name:"
                                   << stateChange.functionGroupName + "." + stateChange.newStateName;
            foundConfigurationError = true;
        }

        if (foundConfigurationError) {
            result.status = StateClientImpl::SetStateResponse::Status::kIgnored;
            GetLogger().LogInfo() << "Cancelling state transition because of a configuration error.";
            return result;
        }

        processedRequest.functionGroup = &(groupIt->second);
        processedRequest.functionGroup->SetCurrentState(processedRequest.state);
        processedRequest.transitionChanges
            = CalculateTransitions(processedRequest.functionGroup, processedRequest.state);

        processedRequest.ongoingSetState = true;
    }

    StateClientImpl::StateChangeStatus changeStatus = StateClientImpl::StateChangeStatus::kSuccess;

    /// @uptrace{SWS_EM_01060, ef3b50c9bb9ddff5ca66ddd7da26a11c59b03402}
    /// @uptrace{SWS_EM_02251, 4cace98c3a1d2d35f860ae00206317c89ef1d688}
    bool terminateComplete = Terminate(processedRequest.transitionChanges.toTerminate_, processedRequest);
    if (!terminateComplete) {
        return {};
    }

    /// @uptrace{SWS_EM_01066, 7fe6ff0f1c82fcc2d5bbe49de79b00786df4eb08}
    bool completed = Execute(processedRequest.transitionChanges.toStart_, processedRequest);
    if (!completed) {
        return {};
    }

    if (processedRequest.failureInformation) {
        const std::shared_ptr<StartupConfiguration> failedProcess = processedRequest.failureInformation->first;
        SetStateFailureCause cause = processedRequest.failureInformation->second;
        switch (cause) {
        case SetStateFailureCause::kEnterTimeout:
            GetLogger().LogError() << "State transition of FunctionGroup" << processedRequest.functionGroup->GetName()
                                   << "to state" << processedRequest.state << "failed due to enter timeout in process"
                                   << failedProcess->GetName();
            changeStatus = StateClientImpl::StateChangeStatus::kFailed;
            processedRequest.stateFailureOccured = true;
            break;

        case SetStateFailureCause::kConfigurationError:
            GetLogger().LogError() << "State transition of FunctionGroup" << processedRequest.functionGroup->GetName()
                                   << "to state" << processedRequest.state
                                   << "failed due to misconfiguration in process" << failedProcess->GetName();
            changeStatus = StateClientImpl::StateChangeStatus::kFailed;
            processedRequest.stateFailureOccured = true;
            break;

        case SetStateFailureCause::kUnexpectedTerminationOnEnter:
            GetLogger().LogError() << "State transition of FunctionGroup" << processedRequest.functionGroup->GetName()
                                   << "to state" << processedRequest.state
                                   << "failed due to unexpected termination of process" << failedProcess->GetName();
            changeStatus = StateClientImpl::StateChangeStatus::kUnexpectedTerminationOnEnter;
            processedRequest.unexpectedTerminationOnEnterOccured = true;
            break;

        default:
            GetLogger().LogError() << "Unknown failure during start of process" << failedProcess->GetName();
            changeStatus = StateClientImpl::StateChangeStatus::kFailed;
            processedRequest.stateFailureOccured = true;
            break;
        }
        processedRequest.functionGroup->SetUndefinedFunctionGroupState(failedProcess->GetExecutionError());
        GetLogger().LogError() << "FunctionGroup" << processedRequest.functionGroup->GetName()
                               << "set to Undefined Function Group state due to process" << failedProcess->GetName();
    }

    if (changeStatus == StateClientImpl::StateChangeStatus::kSuccess) {
        std::string logString(
            "State " + processedRequest.functionGroup->GetName() + "." + processedRequest.state + " ");

        if (!processedRequest.functionGroup->IsTransitionFinished()) {
            changeStatus = StateClientImpl::StateChangeStatus::kTimeout;
            processedRequest.timeoutOccured = true;
            logString += "failed because of timeout";
            GetLogger().LogError() << logString;
        } else {
            processedRequest.functionGroup->UpdateState();

            if (processedRequest.functionGroup->IsCurrentStateFailed()) {
                changeStatus = StateClientImpl::StateChangeStatus::kFailed;
                processedRequest.stateFailureOccured = true;
                logString += "failed because of occured errors.";
                GetLogger().LogError() << logString;
            } else {
                logString += "successfully activated.";
                GetLogger().LogInfo() << logString;
            }
        }
    }

    result.requestResult = changeStatus;
    RemoveTerminatedProcesses();

    /// @uptrace{SWS_EM_01067, b77fba0b292ca01cdefa6edabf1cc5fcc96a7e3d}
    if (!processedRequest.stateFailureOccured && !processedRequest.timeoutOccured
        && !processedRequest.unexpectedTerminationOnEnterOccured) {
        result.status = StateClientImpl::SetStateResponse::Status::kSuccess;
    } else if (processedRequest.stateFailureOccured) {
        result.status = StateClientImpl::SetStateResponse::Status::kFailed;
    } else if (processedRequest.timeoutOccured) {
        result.status = StateClientImpl::SetStateResponse::Status::kTimeout;
    } else if (processedRequest.unexpectedTerminationOnEnterOccured) {
        result.status = StateClientImpl::SetStateResponse::Status::kUnexpectedTerminationOnEnter;
    }

    processedRequest.ongoingSetState = false;
    return result;
#endif
    return true;
}

//StateClientImpl::SetStateResponse::Status 
bool StateChangeHandler::SetCompleteState(
    ProcessedSetStateRequest& processedRequest)
{
#if 0
    netaos::nosal::core::Optional<StateClientImpl::SetStateResponse> responseOptional = {};
    while (!responseOptional.has_value()) {
        responseOptional = SetState(processedRequest);
    }

    return responseOptional->status;
#endif
    return true;
}

bool StateChangeHandler::Execute(std::vector<std::shared_ptr<StartupConfiguration>>& startupConfigurations,
    ProcessedSetStateRequest& processedRequest)
{
    // set up variables if this is the first Execute Call during a SetState operation.
    if (!processedRequest.ongoingExecute) {
        processedRequest.ongoingExecute = true;

        // Print startup configurations
        for (const auto startupConfig : startupConfigurations) {
            GetLogger().LogInfo() << "Starting StartupConfiguration" << startupConfig->GetName();
        }

        processedRequest.startupCheckResults.clear();
        processedRequest.maxNumberOfRetries.clear();
        processedRequest.numberOfAttempts.clear();
        processedRequest.startedAndTerminated.clear();

        for (const auto startupConfiguration : startupConfigurations) {
            processedRequest.startupCheckResults.insert({startupConfiguration->GetName(), StartupStatus::kReady});
            processedRequest.maxNumberOfRetries.insert(
                {startupConfiguration->GetName(), startupConfiguration->GetNumberOfRestartAttempts()});
            processedRequest.numberOfAttempts.insert({startupConfiguration->GetName(), 0});
            processedRequest.startedAndTerminated.insert({startupConfiguration->GetName(), false});
        }
    }

    bool allStartupConfigurationStarted = !(std::any_of(processedRequest.startupCheckResults.cbegin(),
        processedRequest.startupCheckResults.cend(),
        [](const auto& p) { return p.second >= StartupStatus::kReady; }));
    if (allStartupConfigurationStarted) {
        processedRequest.ongoingExecute = false;
        return true;
    }

    auto it = startupConfigurations.begin();
    while (it != startupConfigurations.end()) {
        const std::string& startupConfigurationName = (*it)->GetName();
        auto& startupConfigurationStatus = processedRequest.startupCheckResults.at(startupConfigurationName);

        switch (startupConfigurationStatus) {
        case StartupStatus::kFailed:
            processedRequest.ongoingExecute = false;
            processedRequest.failureInformation = std::make_pair(*it, SetStateFailureCause::kNotStarted);
            return true;
        case StartupStatus::kRunning:
            break;
        case StartupStatus::kReady: {
            // Check if all required Startup Configurations reached their required state
            /// @uptrace{SWS_EM_02245, 0483514ac31495d6c3010e48756c73091682e798}
            StartupConfiguration::DependencyCollectionType dependencies((*it)->GetDependencies());
            while (!dependencies.empty()) {
#if 0
                StartupConfiguration::ProcessExecutionStateReports(executed_startup_configurations_);
#endif

                const std::string& processName = dependencies.cbegin()->first;
                const ProcessState requiredState = dependencies.cbegin()->second;

                GetLogger().LogDebug() << "StartupConfiguration" << startupConfigurationName << "depends on"
                                       << processName << "." << processStateString[requiredState];

                bool dependencyIsOkButDoesNotExistAnymore = processedRequest.startedAndTerminated.count(processName) > 0
                    && processedRequest.startedAndTerminated.at(processName)
                    && requiredState == ProcessState::kTerminated;
                if (dependencyIsOkButDoesNotExistAnymore) {
                    dependencies.erase(dependencies.cbegin());
                    continue;
                }

                const auto requiredConfigurationIt = std::find_if(executed_startup_configurations_.cbegin(),
                    executed_startup_configurations_.cend(),
                    [processName](const std::shared_ptr<StartupConfiguration> configuration) {
                        return configuration->GetName() == processName;
                    });

                // If there is no such dependency, check for configuration error and wait for next loop.
                if (executed_startup_configurations_.cend() == requiredConfigurationIt) {
                    // If it doesn't exist in the startupConfigurations, there is a configuration error.
                    /// @uptrace{SWS_EM_01001, e04db8fba68c4704f1b2811f0672da6bbe74b6cd}
                    if (processedRequest.startupCheckResults.count(processName) == 0) {
                        GetLogger().LogError() << "Execution Dependency error: Process" << startupConfigurationName
                                               << "depends on absent Process" << processName;
                        processedRequest.ongoingExecute = false;
                        processedRequest.failureInformation
                            = std::make_pair(*it, SetStateFailureCause::kConfigurationError);
                        return true;
                    }
                    GetLogger().LogDebug() << "Required process" << processName << "is not started yet,"
                                           << "process" << startupConfigurationName << "depends on it.";
                    break;
                }

                std::shared_ptr<StartupConfiguration> requiredConfiguration = *requiredConfigurationIt;

                // If the dependency reached the required state, then the dependency is satisfied.
                // The dependency can only be fullfilled by a process in the kRunning StartupStatus. A process that
                // needs a restart attempt because of an enter timeout never reached kRunning.
                /// @uptrace{SWS_EM_02280, 28ab471bbde699e1fda3c1aaedf201309246ac80}
                if (requiredConfiguration->GetState() == requiredState
                    && (processedRequest.startupCheckResults.count(processName) == 0
                        || processedRequest.startupCheckResults.at(processName) == StartupStatus::kRunning)) {
                    dependencies.erase(dependencies.cbegin());
                } else if (requiredConfiguration->IsEnterTimeoutOver()) {
                    // If the enter timeout occurs, the dependency will be ignored.
                    GetLogger().LogError()
                        << "Enter timeout is detected for required process" << requiredConfiguration->GetName();
                    dependencies.erase(dependencies.cbegin());
                } else {
                    // Wait for the dependency
                    GetLogger().LogDebug()
                        << "Process" << startupConfigurationName << "is deferred because process"
                        << requiredConfiguration->GetName() << "with pid" << requiredConfiguration->GetPid()
                        << "has not reached required state" << processStateString[requiredState];
                    break;
                }
            }

            if (!dependencies.empty()) {
                break;
            }

            // All dependencies satisfied
            bool successfulStart = (*it)->Start();
            if (!successfulStart) {
                if (processedRequest.numberOfAttempts.at(startupConfigurationName)
                    < processedRequest.maxNumberOfRetries.at(startupConfigurationName)) {
                    processedRequest.numberOfAttempts.at(startupConfigurationName)++;
                } else {
                    startupConfigurationStatus = StartupStatus::kFailed;
                    processedRequest.ongoingExecute = false;
                    processedRequest.failureInformation = std::make_pair((*it), SetStateFailureCause::kNotStarted);
                    return true;
                }
                GetLogger().LogError() << "Failed to start Process" << startupConfigurationName;
                break;
            }

            executed_startup_configurations_.insert((*it));
            startupConfigurationStatus = StartupStatus::kStarting;

            GetLogger().LogInfo() << "Process" << startupConfigurationName << "was started.";

            break;
        }
        case StartupStatus::kStarting:
#if 0
            StartupConfiguration::ProcessExecutionStateReports(executed_startup_configurations_);
#endif

            // check whether process has reached state Running
            if ((*it)->GetState() < ProcessState::kRunning) {
                if ((*it)->IsEnterTimeoutOver()) {
                    GetLogger().LogError() << "Enter timeout is detected for Process" << (*it)->GetName();
                    /// @uptrace{SWS_EM_02259, 48d89a971336524c8bac960d73d0b537f7af9fe4}
                    if (processedRequest.maxNumberOfRetries.at(startupConfigurationName)
                        == processedRequest.numberOfAttempts.at(startupConfigurationName)) {
                        GetLogger().LogError()
                            << "Failed to start Process" << (*it)->GetName() << "due to enter timeout.";
                    }
                    // Terminate the existing process
                    /// @uptrace{SWS_EM_02310, ad139de5ba9d6bcfdea052709e685c5026e0ffac}
                    /// @uptrace{SWS_EM_02312, ea9c6cec971139a114a3307eedfb8946bd361e93}
                    (*it)->Term();
                    startupConfigurationStatus = StartupStatus::kTerminating;
                    GetLogger().LogInfo() << "Terminating Process" << (*it)->GetName() << "due to enter timeout.";
                    break;
                }

                GetLogger().LogDebug() << "Waiting for Process" << (*it)->GetName();

            } else if ((*it)->GetState() == ProcessState::kRunning) {
                GetLogger().LogDebug() << "Process" << (*it)->GetName() << "had reached ProcessState Running.";
                startupConfigurationStatus = StartupStatus::kRunning;
                continue;
            } else if ((*it)->GetState() == ProcessState::kTerminated) {
                /// @uptrace{SWS_EM_02313, d036c395026641e6bfb1170c3189f1be07761d47}
                if ((*it)->GetTerminationCause() == TerminationCause::kAbnormalExit) {
                    GetLogger().LogError()
                        << "Process" << (*it)->GetName() << "terminated unexpectedly during Startup.";
                    processedRequest.ongoingExecute = false;
                    processedRequest.failureInformation
                        = std::make_pair((*it), SetStateFailureCause::kUnexpectedTerminationOnEnter);
                    return true;
                }

                processedRequest.startedAndTerminated.at(startupConfigurationName) = true;
                startupConfigurationStatus = StartupStatus::kRunning;
                continue;
            }
            break;
        case StartupStatus::kTerminating:
            // Detect startup configurations reached ProcessState kTerminating or exit timeout
#if 0
            StartupConfiguration::ProcessExecutionStateReports(executed_startup_configurations_);
#endif

            if ((*it)->GetState() < ProcessState::kTerminated) {
                /// @uptrace{SWS_EM_02258, d2731e5915d79a25477033f1e93842e2cd2472c2}
                if ((*it)->IsExitTimeoutOver()) {
                    GetLogger().LogError()
                        << "Process" << (*it)->GetName() << "will be killed because of exit timeout.";
                    (*it)->Kill();
                    startupConfigurationStatus = StartupStatus::kTerminated;
                }
                break;
            }

            startupConfigurationStatus = StartupStatus::kTerminated;
            break;
        case StartupStatus::kTerminated:
            // Restart terminated process
            /// @uptrace{SWS_EM_02260, 629ee87cb25a82cc9b7d6b872bcddbc9431879d3}
            if (processedRequest.maxNumberOfRetries.at(startupConfigurationName)
                > processedRequest.numberOfAttempts.at(startupConfigurationName)) {
                processedRequest.numberOfAttempts.at(startupConfigurationName)++;
                GetLogger().LogWarn() << "Attempt #" << processedRequest.numberOfAttempts.at(startupConfigurationName)
                                      << "as Process" << (*it)->GetName() << "has not started in the specified time.";
                startupConfigurationStatus = StartupStatus::kReady;
                break;
            }

            startupConfigurationStatus = StartupStatus::kFailed;
            GetLogger().LogError() << "Process" << (*it)->GetName() << "has been terminated because of enter-timeout.";

            processedRequest.ongoingExecute = false;
            processedRequest.failureInformation = std::make_pair((*it), SetStateFailureCause::kEnterTimeout);
            return true;
        default:
            GetLogger().LogError() << "Detected unexpected condition during restarting processes.";
            processedRequest.ongoingExecute = false;
            processedRequest.failureInformation = std::make_pair((*it), SetStateFailureCause::kNotStarted);
        }
        it++;
    }

    return false;
}

bool StateChangeHandler::Terminate(std::vector<std::shared_ptr<StartupConfiguration>>& startupConfigurations,
    ProcessedSetStateRequest& processedRequest)
{
    if (!processedRequest.ongoingTerminate) {
        processedRequest.ongoingTerminate = true;

        // Print startup configurations
        for (const auto startupConfiguration : startupConfigurations) {
            GetLogger().LogInfo() << "Terminating Process" << startupConfiguration->GetName() << "with pid"
                                  << startupConfiguration->GetPid();
        }
    }

    std::size_t iterations = 0;
    constexpr std::size_t maxIterations = 5;

    // This is logic is straightforward and can be improved
    auto it = startupConfigurations.begin();
    while (it != startupConfigurations.end()) {
        if (iterations >= maxIterations) {
            return false;
        }

        (*it)->Term();

        std::size_t waitLoopCount = 0;
        while ((*it)->GetState() != ProcessState::kTerminated) {
            CheckProcessTerminations();

            if ((*it)->IsExitTimeoutOver()) {
                GetLogger().LogError() << "Process" << (*it)->GetName() << "will be killed because of exit timeout";

                /// @uptrace{SWS_EM_02255, 6df69395b1f4ec2594588d07f9f2ab20bd556eed}
                (*it)->Kill();
            }
            ++waitLoopCount;
        }

        GetLogger().LogInfo() << "Waited for termination of Process" << (*it)->GetName() << (*it)->GetPid()
                              << waitLoopCount << "iterations";

        auto executedIt = executed_startup_configurations_.find(*it);
        executed_startup_configurations_.erase(executedIt);
        it = startupConfigurations.erase(it);
        iterations += waitLoopCount;
    }
    processedRequest.ongoingTerminate = false;
    return true;
}

TransitionChanges StateChangeHandler::CalculateTransitions(const FunctionGroup* functionGroup,
    const std::string& state)
{
    RemoveTerminatedProcesses();
    DependencyCalculator dependencyCalculator;
    return dependencyCalculator.CalculateTransition(
        function_groups_, executed_startup_configurations_, functionGroup, state);
}

void StateChangeHandler::RemoveTerminatedProcesses()
{
    for (auto it = executed_startup_configurations_.begin(); it != executed_startup_configurations_.cend();) {

        std::shared_ptr<StartupConfiguration> startupConfiguration = *it;

        if (startupConfiguration->GetState() == ProcessState::kTerminated) {

            it = executed_startup_configurations_.erase(it);

            GetLogger().LogInfo() << "Process" << startupConfiguration->GetName() << "with pid"
                                  << startupConfiguration->GetPid()
                                  << "was removed from the list of executed processes";
        } else {
            ++it;
        }
    }
}

void StateChangeHandler::CheckProcessTerminations()
{
    if (!executed_startup_configurations_.empty()) {
#if 0
        StartupConfiguration::CheckProcesses(executed_startup_configurations_);
#endif
    }
}

}  // namespace internal
}  // namespace exec
}  // namespace nosal
}  // namespace netaos
